package ch.exm.storm.introspection;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;

import ch.exm.storm.log.Logger;

public class IntrospectionUtil {

	private IntrospectionUtil() {}

	private static Map<String, Class<?>> classByNameCache = new HashMap<String, Class<?>>();
	private static Map<Class<?>, Map<String, PropertyDescriptor>> propertyDescriptorCache = new HashMap<Class<?>, Map<String, PropertyDescriptor>>();
	private static Map<Class<?>, Map<String, Field>> fieldsCache = new HashMap<Class<?>, Map<String, Field>>();
	private static Map<Class<?>, Collection<Class<?>>> ancestorsCache = new HashMap<Class<?>, Collection<Class<?>>>();

	public static Object getTemplate(Class<?> clazz) {
		if (clazz == null) return null;
		Object template = null;
		try {
			template = clazz.newInstance();
		} catch (Exception e) {}
		return template;
	}

	public static Object getTemplate(String className) {
		return getTemplate(getClassByName(className));
	}

	public static Class<?> getClassByName(String className) {
		Class<?> clazz = classByNameCache.get(className);
		if (clazz == null) {
			try {
				clazz = Class.forName(className);
				classByNameCache.put(className, clazz);
			} catch (ClassNotFoundException e) {
				Logger.log(IntrospectionUtil.class, Logger.WARN, "Unknown class: " + className);
			}
		}
		return clazz;
	}

	// ////////////////////////////////////////////////////////////////////////
	// /// Methods Introspection
	// ////////////////////////////////////////////////////////////////////////

	public static String getPropertyNameFromAccessor(String accessorMethod) {
		if (accessorMethod.startsWith("get") || accessorMethod.startsWith("set")) 
			return accessorMethod.substring(3, 4).toLowerCase() + accessorMethod.substring(4); 
		if (accessorMethod.startsWith("is")) 
			return accessorMethod.substring(2, 3).toLowerCase() + accessorMethod.substring(3);
		return null;
	}
	
	public static Object call(Object bean, String propertyName) {
		if (bean == null) return null;
		int dotPos = propertyName.indexOf('.');
		if (dotPos > 0) return call(call(bean, propertyName.substring(0, dotPos)), propertyName.substring(dotPos+1));

		PropertyDescriptor propDesc = getPropertyDescriptor(propertyName, getUnproxiedClasses(bean));
		return isReadableProperty(propDesc) ? getPropertyValue(bean, propertyName, propDesc) : invoke(bean, propertyName);
	}

	public static Object invoke(Object bean, String methodName) {
		return invoke(bean, methodName, null);
	}

	public static Object invoke(Object bean, String methodName, Object[] args) {
		Method m = getMethod(bean.getClass(), methodName, args);
		if (m != null) {
			try {
				return m.invoke(bean, args);
			} catch (Exception e) {
				String msg = "Exception invoking method " + methodName + " on object " + bean + " caused by " + e.getLocalizedMessage();
				Logger.log(IntrospectionUtil.class, Logger.WARN, msg);
			}
		} else {
			String msg = "Unable to find method " + methodName + " on object " + bean;
			Logger.log(IntrospectionUtil.class, Logger.WARN, msg);
		}

		return null;
	}

	public static Method getMethod(Class<?> clazz, String methodName, Object[] args) {
		return getMethod(Arrays.asList(clazz.getMethods()), clazz, methodName, args);
	}

	public static Method getMethod(Iterable<Method> methods, Class<?> clazz, String methodName, Object[] args) {
		Method method = null;
		for (Method m : methods) {
			if (!m.getName().equals(methodName)) continue;
			Class<?>[] paramTypes = m.getParameterTypes();
			if (paramTypes.length == 0 && args != null && args.length > 0) continue;
			if (paramTypes.length > 0 && (args == null || args.length == 0)) continue;
			boolean matchingCall = paramTypes.length == 0 && (args == null || args.length == 0);
			if (!matchingCall && args != null && paramTypes.length == args.length) {
				matchingCall = true;
				for (int i = 0; i < args.length; i++) {
					if (args[i] != null && !paramTypes[i].isAssignableFrom(args[i].getClass())) {
						matchingCall = false;
						break;
					}
				}
			}
			if (matchingCall) {
				method = m;
				break;
			}
		}
		return method;
	}

	// ////////////////////////////////////////////////////////////////////////
	// /// Fields Introspection
	// ////////////////////////////////////////////////////////////////////////
	
	public static boolean hasField(Class<?> clazz, String name) {
		return getFieldMap(clazz).get(name) != null;
	}
	
	public static boolean hasField(Object bean, String name) {
		return getFieldMap(bean).get(name) != null;
	}
	
	public static Field getField(Class<?> clazz, String name) {
		return getFieldMap(clazz).get(name);
	}
	
	public static Field getField(Object bean, String name) {
		return getFieldMap(bean).get(name);
	}
	
	public static Collection<Field> getAllFields(Class<?> clazz) {
		return getFieldMap(clazz).values();
	}
	
	public static Collection<Field> getAllFields(Object bean) {
		return getFieldMap(bean).values();
	}
	
	private static Map<String, Field> getFieldMap(Object bean) {
		return getFieldMap(bean.getClass());
	}
	
	private static Map<String, Field> getFieldMap(Class<?> clazz) {
		Map<String, Field> fieldMap = fieldsCache.get(clazz);
		if (fieldMap == null) {
			fieldMap = indexFields(clazz);
			fieldsCache.put(clazz, fieldMap);
		}
		return fieldMap;
	}

	// ////////////////////////////////////////////////////////////////////////
	// /// Properties Introspection
	// ////////////////////////////////////////////////////////////////////////

	public static boolean hasReadableProperty(Object bean, String propertyName) {
		return hasReadableProperty(propertyName, getUnproxiedClasses(bean));
	}

	public static boolean hasReadableProperty(String propertyName, Class<?> ... beanClasses) {
		PropertyDescriptor propDesc = getPropertyDescriptor(propertyName, beanClasses);
		return isReadableProperty(propDesc);
	}

	public static boolean isReadableProperty(PropertyDescriptor propDesc) {
		return propDesc != null ? propDesc.getReadMethod() != null : false;
	}

	public static boolean hasWriteableProperty(Object bean, String propertyName) {
		return hasWriteableProperty(propertyName, getUnproxiedClasses(bean));
	}

	public static boolean hasWriteableProperty(String propertyName, Class<?> ... beanClasses) {
		PropertyDescriptor propDesc = getPropertyDescriptor(propertyName, beanClasses);
		return isWriteableProperty(propDesc);
	}

	public static boolean isWriteableProperty(PropertyDescriptor propDesc) {
		return propDesc != null ? propDesc.getWriteMethod() != null : false;
	}

	// --- (Getters) ----------------------------------------------------------

	public static Object getPropertyValue(Object bean, String propertyName) {
		return getPropertyValue(bean, propertyName, getUnproxiedClasses(bean));
	}

	public static Object getPropertyValue(Object bean, String propertyName, Class<?> ... beanClasses) {
		return getPropertyValue(bean, propertyName, getPropertyDescriptor(propertyName, beanClasses));
	}

	public static Object getPropertyValue(Object bean, String propertyName, PropertyDescriptor propertyDescriptor) {
		Object value = null;
		try {
			value = propertyDescriptor.getReadMethod().invoke(bean, (Object[]) null);
		} catch (Exception e) {
			String msg = "Unable to get value of property " + propertyName + " from bean " + bean + " caused by " + e.getLocalizedMessage();
			Logger.log(IntrospectionUtil.class, Logger.DEBUG, msg);
		}
		return value;
	}

	// --- (Setters) ----------------------------------------------------------

	public static void setPropertyValue(Object bean, String propertyName, Object value) {
		setPropertyValue(bean, propertyName, value, getUnproxiedClasses(bean));
	}

	public static void setPropertyValue(Object bean, String propertyName, Object value, Class<?> ... beanClasses) {
		setPropertyValue(bean, propertyName, getPropertyDescriptor(propertyName, beanClasses), value);
	}

	public static void setPropertyValue(Object bean, String propertyName, PropertyDescriptor propertyDescriptor, Object value) {
		try {
			value = propertyDescriptor.getWriteMethod().invoke(bean, value);
		} catch (Exception e) {
			String msg = "Unable to set value of property " + propertyName + " to bean " + bean + " caused by " + e.getLocalizedMessage();
			Logger.log(IntrospectionUtil.class, Logger.INFO, msg);
		}
	}

	// --- (PropertyDescriptors) ----------------------------------------------

	public static PropertyDescriptor getPropertyDescriptor(String propertyName, Class<?> ... beanClasses) {
		for (Class<?> clazz : beanClasses) {
			Map<String, PropertyDescriptor> propertyDescriptorMap = getPropertyDescriptorMap(clazz);
			PropertyDescriptor propertyDescriptor = propertyDescriptorMap.get(propertyName);
			if (propertyDescriptor != null) return propertyDescriptor;
		}
		return null;
	}

	private static Map<String, PropertyDescriptor> getPropertyDescriptorMap(Class<?> clazz) {
		Map<String, PropertyDescriptor> propertyDescriptorMap = propertyDescriptorCache.get(clazz);
		if (propertyDescriptorMap == null) {
			propertyDescriptorMap = indexPropertyDescriptors(clazz);
			propertyDescriptorCache.put(clazz, propertyDescriptorMap);
		}
		return propertyDescriptorMap;
	}

	public static void copyProperties(Object source, Object destination) {
		copyProperties(source.getClass(), source, destination.getClass(), destination);
	}

	public static void copyProperties(Class<?> sourceClass, Object source, Class<?> destinationClass, Object destination) {
		Map<String, PropertyDescriptor> sourcePropertyDescriptorMap = getPropertyDescriptorMap(sourceClass);
		Map<String, PropertyDescriptor> destinationPropertyDescriptorMap = getPropertyDescriptorMap(destinationClass);

		for (String destinationPropertyName : destinationPropertyDescriptorMap.keySet()) {
			PropertyDescriptor destinationProperty = destinationPropertyDescriptorMap.get(destinationPropertyName);
			if (destinationProperty.getWriteMethod() == null) continue;

			PropertyDescriptor sourceProperty = sourcePropertyDescriptorMap.get(destinationPropertyName);
			if (sourceProperty == null || sourceProperty.getReadMethod() == null) continue;
			try {
				Object sourcePropertyValue = sourceProperty.getReadMethod().invoke(source, (Object[]) null);
				Object narrowedValue = narrow(sourceProperty, destinationProperty, sourcePropertyValue);
				BeanUtils.setProperty(destination, destinationPropertyName, narrowedValue);
			} catch (Exception e) {
				if (Logger.isDebugEnabledFor(IntrospectionUtil.class)) Logger.debug(IntrospectionUtil.class, "Unable to set the property: "
						+ destinationPropertyName + " on: " + destinationClass.getName() + " caused by: " + e.getLocalizedMessage());
			}
		}
	}

	// ////////////////////////////////////////////////////////////////////////
	// /// Class Introspection
	// ////////////////////////////////////////////////////////////////////////

	public static Class<?>[] getUnproxiedClasses(Object bean) {
		return Proxy.isProxyClass(bean.getClass()) ? bean.getClass().getInterfaces() : new Class[]{bean.getClass()};
	}

	public static Collection<Class<?>> getAllAncestors(Class<?> clazz) {
		Collection<Class<?>> ancestors = ancestorsCache.get(clazz);
		if (ancestors == null) {
			ancestors = new LinkedHashSet<Class<?>>();
			indexAncestors(clazz, ancestors);
			ancestorsCache.put(clazz, ancestors);
		}
		return ancestors;
	}

	private static Object narrow(PropertyDescriptor sourceProperty, PropertyDescriptor destinationProperty, Object value) {
		if (value == null) return value;
		if (value instanceof Timestamp && destinationProperty.getPropertyType().equals(Date.class)) {
			value = new Date(((Timestamp) value).getTime());
		}
		return value;
	}

	private static void indexAncestors(Class<?> clazz, Collection<Class<?>> ancestors) {
		if (clazz == null) return;
		ancestors.add(clazz);
		for (Class<?> interfaze : clazz.getInterfaces())
			indexAncestors(interfaze, ancestors);
		indexAncestors(clazz.getSuperclass(), ancestors);
	}

	private static Map<String, PropertyDescriptor> indexPropertyDescriptors(Class<?> clazz) {
		Map<String, PropertyDescriptor> propertyDescriptorMap = new HashMap<String, PropertyDescriptor>();
		for (Class<?> ancestorClazz : getAllAncestors(clazz)) {
			PropertyDescriptor[] propDescs = PropertyUtils.getPropertyDescriptors(ancestorClazz);
			for (PropertyDescriptor propDesc : propDescs) {
				String propertyName = propDesc.getName();
				PropertyDescriptor superPropDesc = propertyDescriptorMap.get(propertyName);
				if (superPropDesc != null) {
					Method readMethod = propDesc.getReadMethod();
					if (readMethod == null) {
						try {
							propDesc.setReadMethod(superPropDesc.getReadMethod());
						} catch (IntrospectionException e) {
							if (Logger.isDebugEnabledFor(IntrospectionUtil.class)) Logger.debug(IntrospectionUtil.class,
									"Unable to index the read method of the property: " + propertyName + " caused by: " + e.getLocalizedMessage());
						}
					}
					Method writeMethod = propDesc.getWriteMethod();
					if (writeMethod == null) {
						try {
							propDesc.setWriteMethod(superPropDesc.getWriteMethod());
						} catch (IntrospectionException e) {
							if (Logger.isDebugEnabledFor(IntrospectionUtil.class)) Logger.debug(IntrospectionUtil.class,
									"Unable to index the write method of the property: " + propertyName + " caused by: " + e.getLocalizedMessage());
						}
					}
				}
				propertyDescriptorMap.put(propertyName, propDesc);
			}
		}
		return propertyDescriptorMap;
	}
	
	private static Map<String, Field> indexFields(Class<?> clazz) {
		Map<String, Field> fieldMap = new HashMap<String, Field>();
		for (Class<?> ancestorClazz : getAllAncestors(clazz)) {
			if (ancestorClazz.isInterface()) continue;
			for (Field field : ancestorClazz.getDeclaredFields()) fieldMap.put(field.getName(), field);
		}
		return fieldMap;
	}
}
